﻿//Copyright (c) 2010, Andreas Grünwald
//All rights reserved.

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Security;
using System.Text;
using System.Web;
using System.Windows;
using System.Xml.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Slf;
using yaTDL.Extensions;
using yaTDL.Json;

namespace yaTDL.Core.Toodledo
{

    //for Toodledeo API Documentation see http://api.toodledo.com/2/index.php
    public class ToodledoClientV2
    {
        static string toodledoBaseUrl = "http://api.toodledo.com/2/";

        static string appId = "yaTDLv2";           //TODO: Insert AppId and AppToken
        static string appToken = "api4e036a8a30acb";
        static string appVersion = Assembly.GetExecutingAssembly().FullName.Split(',')[1].Substring(9).Replace(".", "");
        static string device = "Windows_Client";
        static string osVersion = Environment.OSVersion.Version.ToString().Replace(".", "");


        WebClient webClient = new WebClient();

        ILogger logger = LoggerService.GetLogger("ToodledoClient");


        public event EventHandler<ErrorEventArgsV2> ErrorOccurred;

        public DateTime FirstRequest { get; set; }



        public ToodledoClientV2()
        {
            FirstRequest = DateTime.Parse("01.01.0001");
        }


        public string GenerateSignature(string userId)
        {
            return (userId + appToken).GetMD5Hash();
        }

        public string GetSessionToken(string userId)
        {            
            string url = String.Format("{0}account/token.php?userid={1};appid={2};vers={3};device={4};os={5};sig={6};f=xml",
                toodledoBaseUrl, userId, appId, appVersion, device, osVersion,GenerateSignature(userId));


            XElement response = makeRequest(url);

            if (response.Name == "token") return response.Value;
            else return null;

        }       

        public ToodledoAPIKey GetKey(string sessionToken, SecureString password)
        {
            ToodledoAPIKey key = new ToodledoAPIKey();
            key.Key = (password.Unwrap().GetMD5Hash() + appToken + sessionToken).GetMD5Hash();
            key.Expires = DateTime.Now.AddHours(3.9);

            return key;
        }

        public ToodledoAccountInfoV2 GetAccountInfo(string key)
        {
            string url = toodledoBaseUrl + "account/get.php?key=" + key + ";f=xml";

            XElement response = makeRequest(url);

            if (response.Name == "account")
            {
                ToodledoAccountInfoV2 accountInfo = new ToodledoAccountInfoV2();
                accountInfo.UserId = response.Element("userid").Value;
                accountInfo.Alias = response.Element("alias").Value;
                if (response.Element("pro").Value == "1") accountInfo.IsProUser = true;
                accountInfo.DateFormat = Int32.Parse(response.Element("dateformat").Value);
                accountInfo.TimeZone = Int32.Parse(response.Element("timezone").Value);
                accountInfo.HideMonths = Int32.Parse(response.Element("hidemonths").Value);
                accountInfo.HotListPriority = Int32.Parse(response.Element("hotlistpriority").Value);
                accountInfo.HotListDueDate = Int32.Parse(response.Element("hotlistduedate").Value);
                accountInfo.ShowTabNums = Int32.Parse(response.Element("showtabnums").Value);
                accountInfo.LastFolderEdit = (double.Parse(response.Element("lastedit_folder").Value)).ToDateTime();
                accountInfo.LastContextEdit = (double.Parse(response.Element("lastedit_context").Value)).ToDateTime();
                accountInfo.LastGoalEdit = (double.Parse(response.Element("lastedit_goal").Value)).ToDateTime();
                accountInfo.LastTaskEdit = (double.Parse(response.Element("lastedit_task").Value)).ToDateTime();
                accountInfo.LastTaskDelete = (double.Parse(response.Element("lastdelete_task").Value)).ToDateTime();

                return accountInfo;
            }
            else
            { return null; }
        }

        public Collection<Project> GetFolders(string key)
        {
            Collection<Project> projects = new Collection<Project>();

            string url = toodledoBaseUrl + "/folders/get.php?key=" + key + ";f=xml";

            XElement xElement = makeRequest(url);

            foreach (XElement x in xElement.Elements("folder"))
            {
                Project p = new Project();
                p.Name = x.Element("name").Value;
                p.Id = x.Element("id").Value;
                p.Order = Int32.Parse(x.Element("order").Value);
                projects.Add(p);
            }

            return projects;
        }

        public bool AddFolder(string key, Project project)
        {
            string url = String.Format("{0}folders/add.php?name={1};key={2};f=xml", toodledoBaseUrl, project.Name, key);

            XElement xElement = makeRequest(url);

            if (xElement.Name == "folders")
            {
                project.Id = xElement.Element("folder").Element("id").Value;
                project.Order = Int32.Parse(xElement.Element("folder").Element("order").Value);
                return true;
            }
            else
            { return false; }

        }

        public bool UpdateFolder(string key, Project project)
        {
            string url = String.Format("{0}folders/edit.php?id={1};name={2};key={3};f=xml", toodledoBaseUrl, project.Id, project.Name, key);

            return makeRequest(url).Name == "folders" ? true : false;
        }

        public bool DeleteFolder(string key, int id)
        {
            string url = String.Format("{0}folders/delete.php?id={1};key={2};f=xml", toodledoBaseUrl, id, key);

            return makeRequest(url).Name == "deleted" ? true : false;

        }

        public Collection<Task> GetTasks(string key, List<Parameter> parameters)
        {
            string url = String.Format("{0}tasks/get.php?key={1};", toodledoBaseUrl, key);


            foreach (Parameter parameter in parameters)
            {
                url += parameter.Name + "=";
                switch (parameter.Name.ToLower())
                {
                    case "id":
                    case "start":
                    case "num":
                        url += parameter.Value.ToString();
                        break;

                    case "modbefore":
                    case "modafter":
                        url += ((DateTime)parameter.Value).ToUnixTime().ToString();
                        break;

                    case "comp":
                        if (parameter.Value.GetType() == typeof(int))
                        {
                            url += ((int)parameter.Value).ToString();
                        }
                        if (parameter.Value.GetType() == typeof(bool))
                        {
                            if ((bool)parameter.Value) { url += "1"; }
                            else { url += "0"; }
                        }
                        break;

                }
                url += ";";
            }

            url += "fields=folder,priority,duedate,repeat,repeatfrom,added;f=xml";



            Collection<Task> tasks = new Collection<Task>();
            XElement response = makeRequest(url);

            if (response.Name == "tasks")
            {
                foreach (XElement item in response.Elements("task"))
                {
                    Task t = new Task();
                    t.Id = item.Element("id").Value;

                    t.Content = urlDecode(item.Element("title").Value);
                    t.ProjectId = item.Element("folder").Value;

                    if (String.IsNullOrEmpty(item.Element("modified").Value))
                    {
                        t.LastEdited = (double.Parse(item.Element("added").Value)).ToDateTime();
                    }
                    else
                    {
                        t.LastEdited = (double.Parse(item.Element("modified").Value)).ToDateTime();
                    }

                    if (String.IsNullOrEmpty(item.Element("duedate").Value) || item.Element("duedate").Value == "0")
                    { t.DueDate = DateTime.Parse("01.01.0001"); }
                    else
                    {
                        t.DueDate = (double.Parse(item.Element("duedate").Value)).ToDateTime().ToLocalTime();
                    }

                    if (String.IsNullOrEmpty(item.Element("completed").Value) || item.Element("completed").Value == "0")
                    { t.Completed = false; }
                    else
                    { t.Completed = true; }

                    t.Recurrence.AdvancedRepeat = item.Element("repeat").Value;
                                        
                    t.Recurrence.Repeat = repeatFromToRepeat(Int32.Parse(item.Element("repeatfrom").Value), t.Recurrence.AdvancedRepeat);                                       
                                

                    t.Priority = Int32.Parse(item.Element("priority").Value);

                    tasks.Add(t);
                }
            }

            return tasks;
        }

        
        public bool AddTasks(string key, List<Task> tasks)
        {
            if (tasks.Count > 20)
            {
                List<List<Task>> taskLists = splitTaskList(20, tasks);

                List<bool> results = new List<bool>();

                foreach (List<Task> taskList in taskLists)
                {
                    results.Add(AddTasks(key, taskList));
                }

                return results.Any(x=> x== false) ? false : true;
            }

            string serializedTasks = jsonSerialize(tasks, includeId:false, addIdAsMeta:true);


            if (!String.IsNullOrEmpty(serializedTasks))
            {

                string url = String.Format("{0}tasks/add.php?key={1};tasks={2};fields=folder,priority,duedate,repeat,meta;f=xml",
                    toodledoBaseUrl, key, serializedTasks);


                XElement response = makeRequest(url);

                if (response.Name == "tasks")
                {
                    foreach (XElement element in response.Elements("task"))
                    {
                        var matchingTasks = from task in tasks
                                            where task.Id == element.Element("meta").Value.Split(';').First(x=> x.StartsWith("localId_original")).Replace("localId_original=", "")
                                            select task;

                        //var matchingTasks = from task in tasks
                        //                    where task.Content == element.Element("title").Value
                        //                    && task.ProjectId == element.Element("folder").Value                                                                                        
                        //                    && task.Recurrence.AdvancedRepeat == element.Element("repeat").Value
                        //                    && task.Recurrence.Repeat == repeatFromToRepeat(Int32.Parse(element.Element("repeatfrom").Value), element.Element("repeat").Value)
                        //                    select task;

                        if (matchingTasks.Any())
                        {
                            if (matchingTasks.Count() > 1)
                            {
                                Core.UI.Dialogs.ShowMessageBox("AddTasks: Found more than one matching Task. This should not have happend");
                                logger.Error("AddTasks - Found more than one matching Task");
                                return false;
                            }

                            matchingTasks.First().Id = element.Element("id").Value;                                                     
                        }
                        else
                        {
                            logger.Error("AddTasks: response did not contain a matching Task");
                            return false;
                        }

                    }
                    return true;
                }                
            }

            return false;
        }

        public bool UpdateTasks(string key, List<Task> tasks)
        {

            if (tasks.Count > 25)
            {
                List<List<Task>> taskLists = splitTaskList(25, tasks);

                List<bool> results = new List<bool>();

                foreach (List<Task> taskList in taskLists)
                {
                    results.Add(UpdateTasks(key, taskList));
                }

                var fails = from result in results
                            where result == false
                            select result;

                return fails.Any() ? false : true;
            }

            string url = String.Format("{0}tasks/edit.php?key={1};tasks={2};f=xml", toodledoBaseUrl, key, jsonSerialize(tasks, true));

            XElement response = makeRequest(url);

            if (response.Name == "tasks")
                return true;
            else
                return false;

        }

        public bool DeleteTasks(string key, List<int> ids)
        {
            string url = String.Format("{0}tasks/delete.php?key={1};tasks={2};f=xml", toodledoBaseUrl, key, JsonConvert.SerializeObject(ids.ToArray()));       

            XElement response = makeRequest(url);

            return response.Name == "deleted" ? true : false;
        }

        public Collection<string> GetDeletedTasks(string key, DateTime after)
        {
            string url = String.Format("{0}tasks/deleted.php?key={1};after={2};f=xml", toodledoBaseUrl, key, after.ToUnixTime().ToString());

            XElement response = makeRequest(url);

            var _deleted = from task in response.Elements("task")
                           select task.Element("id").Value;

            return new Collection<string>(_deleted.ToList<string>());

        }
        
      

        private void OnErrorOccurred(int errorCode, string errorMessage)
        {
            if (ErrorOccurred != null)
            {
                ErrorOccurred(this, new ErrorEventArgsV2() { ErrorCode = errorCode, ErrorMessage= errorMessage });
            }
        }

        int errorCounter = 1;

        private XElement makeRequest(string url)
        {
            if (FirstRequest == DateTime.MinValue) { FirstRequest = DateTime.Now; }

            string responseString ="";


            try
            {
                responseString = webClient.DownloadString(url);
            }
            catch (WebException)
            {
                OnErrorOccurred(-1, "System.Net.WebClient.DownloadString(string) threw an Webexception");   
            }
            
   

            XElement response = XElement.Parse(responseString);

            List<XElement> errors = response.DescendantsAndSelf("error").ToList<XElement>();

            foreach (XElement error in errors)
            {
                OnErrorOccurred(errorCode: Int32.Parse(error.Attribute("id").Value), errorMessage: error.Value);
#if DEBUG
                if (error.Value.ToLower() == "no title")
                {
                    logger.Error("url: {0}", url);

                    File.WriteAllText(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "noTitleErrorUrl_" + errorCounter), url);
                    errorCounter++;
                }
#endif
            }
            
            
            return response;            
        }

        private static List<List<Task>> splitTaskList(int maxLength, List<Task> longList)
        {
            List<List<Task>> result = new List<List<Task>>();
            List<Task> currentList = new List<Task>();
            while (longList.Count > maxLength)
            {
                if (currentList.Count < maxLength)
                {
                    currentList.Add(longList.First());
                    longList.RemoveAt(0);
                }
                else
                {
                    result.Add(currentList);
                    currentList = new List<Task>();
                }
            }
            result.Add(longList);

            return result;
        }        

        [DebuggerStepThrough()]
        private static string jsonSerialize(List<Task> tasks, bool includeId, bool addIdAsMeta = false)
        {
            JsonArray<JsonObject<object>> taskArray = new JsonArray<JsonObject<object>>();

            foreach (Task t in tasks)
            {
                JsonObject<object> jsonTask = new JsonObject<object>();



                if (includeId)
                    jsonTask.AddItem("id", t.Id);

                jsonTask.AddItem("title", t.Content);
                jsonTask.AddItem("priority", t.Priority);

                if(!String.IsNullOrEmpty(t.Recurrence.AdvancedRepeat))
                    jsonTask.AddItem("repeat", t.Recurrence.AdvancedRepeat);


                if (String.IsNullOrEmpty(t.ProjectId))
                    throw new Exception("Task has invalid ProjectId (empty)");

                if (t.ProjectId != "0")
                   jsonTask.AddItem("folder", t.ProjectId);


                t.DueDate = t.DueDate.Date.AddHours(12);    //the time component is always noon

                if (t.DueDate.Date != DateTime.MinValue.Date)
                   jsonTask.AddItem("duedate", t.DueDate.ToUnixTime());

                if (t.Completed)
                    jsonTask.AddItem("completed", t.LastEdited.ToUnixTime());
                else
                    jsonTask.AddItem("completed", 0);


                if (addIdAsMeta)
                {
                    jsonTask.AddItem("meta", "localId_original=" + t.Id);
                }


                taskArray.AddItem(jsonTask);
            }


            return taskArray.ToString();
        }
        
        //[DebuggerStepThrough()]
        //private static string jsonSerialize(Task task, bool includeId, bool addIdAsMeta)
        //{            
        //    if (task == null) return "";

        //    if (String.IsNullOrEmpty(task.Content))
        //    {
        //        throw new Exception();
        //    }

        //    string baseString = "\"{0}\"%3A\"{1}\",";
        //    //string baseString = "{\"{0}\":\"{1}\"";

        //    string result = "{";
            
        //    if (includeId)
        //    {
        //        result += String.Format("\"id\"%3A\"{0}\",", task.Id);
        //        result += String.Format(baseString, "title", task.Content);
        //    }
        //    else
        //    {
        //        result += String.Format("\"title\"%3A\"{0}\",", task.Content.Replace("\\", "\\\\").Replace("\"", "\\\""));
        //    }
            
        //    result += String.Format(baseString, "priority", task.Priority);
        //    result += String.Format(baseString, "repeat", task.Recurrence.Repeat);

        //    if (task.ProjectId != "0")
        //        result += String.Format(baseString, "folder", task.ProjectId);

        //    if (task.DueDate.Date != DateTime.Parse("01.01.0001").Date)
        //        result += String.Format(baseString, "duedate", task.DueDate.ToUnixTime());

        //    if (task.Completed)
        //        result += String.Format(baseString, "completed", task.LastEdited.ToUnixTime());
        //    else
        //        result += String.Format(baseString, "completed", "0");

        //    if (addIdAsMeta)
        //    {
        //        result += String.Format(baseString, "meta", "localId_original=" + task.Id);
        //    }

        //    if(result.EndsWith(","))
        //        result = result.Remove(result.Length -1, 1);

        //    result += "}";

        //    return result;
        //}


        private static int repeatFromToRepeat(int repeatFrom, string advancedRepeat)
        {
            if (String.IsNullOrEmpty(advancedRepeat))
            {
                return 0;
            }
            else
            {
                return repeatFrom == 0 ? 50 : 150;
            }
        }


        private static string urlDecode(string content)
        {
            string result = HttpUtility.UrlDecode(content);
            
            result = result.Replace("Ã¶", "ö");
            result = result.Replace("â‚¬", "€");
            result = result.Replace("Ã¤", "ä");
            result = result.Replace("Ã„", "Ä");
            result = result.Replace("Ã¼", "ü");
            result = result.Replace("ÃŸ", "ß");
            result = result.Replace("Ã–", "Ö");
            result = result.Replace("Ãœ", "Ü");
            result = result.Replace("Â§", "§");
            result = result.Replace("Âµ", "µ");
            result = result.Replace("Â²", "²");

            return result;
        }

    }


    public class Parameter
    {
        public string  Name { get; set; }

        public object Value { get; set; }
    }

 
}
